import bs
import random

def bsGetAPIVersion():
    # see bombsquadgame.com/apichanges
    return 4

def bsGetGames():
    return [RaceGame]

class RaceRegion(bs.Actor):
    def __init__(self,pt,index):
        bs.Actor.__init__(self)
        activity = self.getActivity()
        self._pt = pt
        self._index = index
        self.node = bs.newNode("region",
                               owner=self,
                               delegate=self,
                               attrs={'position':pt[:3],
                                      'scale': (pt[3]*2.0,pt[4]*2.0,pt[5]*2.0),
                                      'type': "box",
                                      'materials':[activity._raceRegionMaterial]})

class RaceGame(bs.TeamGameActivity):

    @classmethod
    def getName(cls):
        return 'Race'

    @classmethod
    def getDescription(cls,sessionType):
        return 'Run real fast!'

    @classmethod
    def getScoreInfo(cls):
        return {'scoreName':'Time',
                'lowerIsBetter':True,
                'scoreType':'milliseconds'}
    
    @classmethod
    def supportsSessionType(cls,sessionType):
        return True if (issubclass(sessionType,bs.TeamsSession)
                        or issubclass(sessionType,bs.FreeForAllSession)) else False

    @classmethod
    def getSupportedMaps(cls,sessionType):
        return bs.getMapsSupportingPlayType("race")

    @classmethod
    def getSettings(cls,sessionType):
        settings = [("Laps",{'minValue':1,"default":3,"increment":1}),
                    ("Time Limit",{'choices':[('None',0),('1 Minute',60),
                                              ('2 Minutes',120),('5 Minutes',300),
                                              ('10 Minutes',600),('20 Minutes',1200)],'default':0}),
                    ("Mine Spawning",{'choices':[('No Mines',0),('8 Seconds',8000),
                                                 ('4 Seconds',4000),('2 Seconds',2000)],'default':4000}),
                    ("Bomb Spawning",{'choices':[('None',0),('8 Seconds',8000),
                                                 ('4 Seconds',4000),('2 Seconds',2000),('1 Second',1000)],'default':2000}),
                    ("Epic Mode",{'default':False})]
        
        if issubclass(sessionType,bs.TeamsSession):
            settings.append(("Entire Team Must Finish",{'default':False}))
        return settings
        
    
    def __init__(self,settings):
        self._raceStarted = False
        bs.TeamGameActivity.__init__(self,settings)
        self._scoreBoard = bs.ScoreBoard()
        if self.settings['Epic Mode']: self._isSlowMotion = True
        self._scoreSound = bs.getSound("score")
        self._swipSound = bs.getSound("swip")
        self._lastTeamTime = None
        self._frontRaceRegion = None

    def getInstanceDescription(self):
        if isinstance(self.getSession(),bs.TeamsSession) and self.settings.get('Entire Team Must Finish', False):
            tStr = ' Your entire team has to finish.'
        else: tStr = ''

        if self.settings['Laps'] > 1: s = ('Run ${ARG1} laps.'+tStr,self.settings['Laps'])
        else: s = 'Run 1 lap.'+tStr
        return s

    def getInstanceScoreBoardDescription(self):
        if self.settings['Laps'] > 1: s = ('run ${ARG1} laps',self.settings['Laps'])
        else: s = 'run 1 lap'
        return s

    def onTransitionIn(self):
        bs.TeamGameActivity.onTransitionIn(self, music='Epic Race' if self.settings['Epic Mode'] else 'Race')

        self._nubTex = bs.getTexture('nub')
        self._beep1Sound = bs.getSound('raceBeep1')
        self._beep2Sound = bs.getSound('raceBeep2')

        pts = self.getMap().getDefPoints('racePoint')

        m = self._raceRegionMaterial = bs.Material()
        m.addActions(conditions=("theyHaveMaterial",bs.getSharedObject('playerMaterial')),
                     actions=(("modifyPartCollision","collide",True),
                              ("modifyPartCollision","physical",False),
                              ("call","atConnect",self._handleRacePointCollide)))

        self._regions = []
        for pt in pts:
            self._regions.append(RaceRegion(pt,len(self._regions)))

    def _flashPlayer(self,player,scale):
        pos = player.actor.node.position
        light = bs.newNode('light',
                           attrs={'position':pos,
                                  'color':(1,1,0),
                                  'heightAttenuated':False,
                                  'radius':0.4})
        bs.gameTimer(500,light.delete)
        bs.animate(light,'intensity',{0:0,100:1.0*scale,500:0})
        
    def _handleRacePointCollide(self):

        regionNode,playerNode = bs.getCollisionInfo('sourceNode','opposingNode')
        try: player = playerNode.getDelegate().getPlayer()
        except Exception: player = None
        region = regionNode.getDelegate()
        if player is None or not player.exists() or region is None: return

        lastRegion = player.gameData['lastRegion']
        thisRegion = region._index

            
        if lastRegion != thisRegion:

            
            # if a player tries to skip regions, smite them
            # ..allow a one region leeway though (its plausable players can get blown over a region, etc)
            if thisRegion > lastRegion + 2:
                if player.isAlive():
                    player.actor.handleMessage(bs.DieMessage())
                    bs.screenMessage(bs.Lstr(translate=('statements',"Killing ${NAME} for skipping part of the track!"),subs=[('${NAME}',player.getName(full=True))]),color=(1,0,0))
            else:

                # if this player is in first, note that this is the front-most race-point
                if player.gameData['rank'] == 0:
                    self._frontRaceRegion = thisRegion
                
                player.gameData['lastRegion'] = thisRegion
                if lastRegion >= len(self._regions)-2 and thisRegion == 0:
                    team = player.getTeam()
                    player.gameData['lap'] = min(self.settings['Laps'],player.gameData['lap'] + 1)

                    # in teams mode with all-must-finish on, the team lap value is the min of all team players
                    # ..otherwise its the max
                    if isinstance(self.getSession(),bs.TeamsSession) and self.settings.get('Entire Team Must Finish'):
                        team.gameData['lap'] = min([p.gameData['lap'] for p in team.players])
                    else:
                        team.gameData['lap'] = max([p.gameData['lap'] for p in team.players])

                    # a player is finishing
                    if player.gameData['lap'] == self.settings['Laps']:

                        # in teams mode, hand out points based on the order players come in
                        if isinstance(self.getSession(),bs.TeamsSession):
                            if self._teamFinishPts > 0:
                                self.scoreSet.playerScored(player,self._teamFinishPts,screenMessage=False)
                            self._teamFinishPts -= 25

                        # flash where the player is
                        self._flashPlayer(player,1.0)
                        player.gameData['finished'] = True
                        player.actor.handleMessage(bs.DieMessage(immediate=True))
                        
                        # makes sure noone behind them passes them in rank while finishing..
                        player.gameData['distance'] = 9999.0

                        # if the whole team has finished the race..
                        if team.gameData['lap'] == self.settings['Laps']:
                            bs.playSound(self._scoreSound)
                            player.getTeam().gameData['finished'] = True
                            self._lastTeamTime = player.getTeam().gameData['time'] = bs.getGameTime()-self._timer.getStartTime()
                            self._checkEndGame()

                        # team has yet to finish..
                        else:
                            bs.playSound(self._swipSound)

                    # they've just finished a lap but not the race..
                    else:
                        bs.playSound(self._swipSound)
                        self._flashPlayer(player,0.3)

                        # print their lap number over their head..
                        try:
                            m = bs.newNode('math',owner=player.actor.node,attrs={'input1':(0,1.9,0),'operation':'add'})
                            player.actor.node.connectAttr('torsoPosition',m,'input2')
                            t = bs.newNode('text',
                                           owner=m,
                                           attrs={'text':bs.Lstr(resource='lapNumberText',
                                                                 subs=[('${CURRENT}',str(player.gameData['lap']+1)),
                                                                       ('${TOTAL}',str(self.settings['Laps']))]),
                                                  'inWorld':True,
                                                  'color':(1,1,0,1),
                                                  'scale':0.015,
                                                  'hAlign':'center'})
                            m.connectAttr('output',t,'position')
                            bs.animate(t,'scale',{0:0,200:0.019,2000:0.019,2200:0})
                            bs.gameTimer(2300,m.delete)
                        except Exception,e:
                            print 'Exception printing lap:',e
                        
    def onTeamJoin(self,team):
        team.gameData['time'] = None
        team.gameData['lap'] = 0
        team.gameData['finished'] = False
        self._updateScoreBoard()

    def onPlayerJoin(self,player):
        player.gameData['lastRegion'] = 0
        player.gameData['lap'] = 0
        player.gameData['distance'] = 0.0
        player.gameData['finished'] = False
        player.gameData['rank'] = None
        bs.TeamGameActivity.onPlayerJoin(self,player)

    def onPlayerLeave(self,player):
        bs.TeamGameActivity.onPlayerLeave(self,player)

        # a player leaving disqualifies the team if 'Entire Team Must Finish' is on
        # (otherwise in teams mode everyone could just leave except the leading player to win)
        if isinstance(self.getSession(),bs.TeamsSession) and self.settings.get('Entire Team Must Finish'):
            # FIXME translate this
            # bs.screenMessage(player.getTeam().name+" is disqualified because "+player.getName(full=True)+" left",color=(1,1,0))
            bs.screenMessage(bs.Lstr(translate=('statements', '${TEAM} is disqualified because ${PLAYER} left'),
                                     subs=[('${TEAM}',player.getTeam().name),
                                           ('${PLAYER}',player.getName(full=True))]),color=(1,1,0))
            player.getTeam().gameData['finished'] = True
            player.getTeam().gameData['time'] = None
            player.getTeam().gameData['lap'] = 0
            bs.playSound(bs.getSound("boo"))
            for player in player.getTeam().players:
                player.gameData['lap'] = 0
                player.gameData['finished'] = True
                try: player.actor.handleMessage(bs.DieMessage())
                except Exception: pass

        # delay by one tick so team/player lists will be updated
        bs.gameTimer(1,self._checkEndGame)

    def _updateScoreBoard(self):
        for team in self.teams:
            distances = [player.gameData['distance'] for player in team.players]
            if len(distances) == 0: teamDist = 0
            else:
                if isinstance(self.getSession(),bs.TeamsSession) and self.settings.get('Entire Team Must Finish'):
                    teamDist = min(distances)
                else:
                    teamDist = max(distances)
            self._scoreBoard.setTeamValue(team,teamDist,self.settings['Laps'],flash=(teamDist >= float(self.settings['Laps'])),showValue=False)

    def onBegin(self):
        bs.TeamGameActivity.onBegin(self)
        self.setupStandardTimeLimit(self.settings['Time Limit'])
        self.setupStandardPowerupDrops()
        self._teamFinishPts = 100

        # throw a timer up on-screen
        self._timeText = bs.NodeActor(bs.newNode('text',
                                                 attrs={'vAttach':'top','hAttach':'center','hAlign':'center',
                                                        'color':(1,1,0.5,1),'flatness':0.5,'shadow':0.5,
                                                        'position':(0,-50),'scale':1.4,'text':''}))
        self._timer = bs.OnScreenTimer()
        
        if self.settings['Mine Spawning'] != 0:
            self._raceMines = [{'point':p,'mine':None} for p in self.getMap().getDefPoints('raceMine')]
            if len(self._raceMines) > 0:
                self._raceMineTimer = bs.Timer(self.settings['Mine Spawning'],self._updateRaceMine,repeat=True)

        self._scoreBoardTimer = bs.Timer(250,self._updateScoreBoard,repeat=True)
        self._playerOrderUpdateTimer = bs.Timer(250,self._updatePlayerOrder,repeat=True)

        if self._isSlowMotion:
            tScale = 0.4
            lightY = 50
        else:
            tScale = 1.0
            lightY = 150
        lStart = int(7100*tScale)
        inc = int(1250*tScale)

        bs.gameTimer(lStart,self._doLight1)
        bs.gameTimer(lStart+inc,self._doLight2)
        bs.gameTimer(lStart+2*inc,self._doLight3)
        bs.gameTimer(lStart+3*inc,self._startRace)

        self._startLights = []
        for i in range(4):
            l = bs.newNode('image',
                           attrs={'texture':bs.getTexture('nub'),
                                  'opacity':1.0,
                                  'absoluteScale':True,
                                  'position':(-75+i*50,lightY),
                                  'scale':(50,50),
                                  'attach':'center'})
            bs.animate(l,'opacity',{4000*tScale:0,5000*tScale:1.0,12000*tScale:1.0,12500*tScale:0.0})
            bs.gameTimer(int(13000*tScale),l.delete)
            self._startLights.append(l)

        self._startLights[0].color = (0.2,0,0)
        self._startLights[1].color = (0.2,0,0)
        self._startLights[2].color = (0.2,0.05,0)
        self._startLights[3].color = (0.0,0.3,0)

    def _doLight1(self):
        self._startLights[0].color = (1.0,0,0)
        bs.playSound(self._beep1Sound)
    def _doLight2(self):
        self._startLights[1].color = (1.0,0,0)
        bs.playSound(self._beep1Sound)
    def _doLight3(self):
        self._startLights[2].color = (1.0,0.3,0)
        bs.playSound(self._beep1Sound)
    def _startRace(self):
        self._startLights[3].color = (0.0,1.0,0)
        bs.playSound(self._beep2Sound)
        for player in self.players:
            if player.actor is not None:
                try:player.actor.connectControlsToPlayer()
                except Exception,e: print 'Exception in race player connects:',e
        self._timer.start()
        
        if self.settings['Bomb Spawning'] != 0:
            self._bombSpawnTimer = bs.Timer(self.settings['Bomb Spawning'],self._spawnBomb,repeat=True)
        
        self._raceStarted = True

    def _updatePlayerOrder(self):

        # calc all player distances
        for player in self.players:
            try: pos = bs.Vector(*player.actor.node.position)
            except Exception: pos = None
            if pos is not None:
                rIndex = player.gameData['lastRegion']
                r1 = self._regions[rIndex]
                r1Pt = bs.Vector(*r1._pt[:3])
                r2 = self._regions[0] if rIndex == len(self._regions)-1 else self._regions[rIndex+1]
                r2Pt = bs.Vector(*r2._pt[:3])
                r1Dist = (pos-r1Pt).length()
                r2Dist = (pos-r2Pt).length()
                amt = 1.0-(r2Dist/(r2Pt-r1Pt).length())
                amt = player.gameData['lap'] + (rIndex+amt) * (1.0/len(self._regions))
                player.gameData['distance'] = amt

        # sort players by distance and update their ranks
        pList = [[player.gameData['distance'],player] for player in self.players]
        pList.sort(reverse=True)
        for i,p in enumerate(pList):
            try:
                p[1].gameData['rank'] = i
                if p[1].actor is not None:
                    n = p[1].actor.distanceTxt
                    if n.exists(): n.text = str(i+1) if p[1].isAlive() else ''
            except Exception:
                bs.printException('error updating player orders')

    def _spawnBomb(self):
        if self._frontRaceRegion is None: return
        region = (self._frontRaceRegion+(3))%len(self._regions)
        #print 'WOULD SPAWN BOMB AT',bs.getGameTime(),'AT REGION',region
        pt = self._regions[region]._pt
        regionScale = 0.8 # dont use the full region so we're less likely to spawn off a cliff
        xRange = (-0.5,0.5) if pt[3] == 0 else (-regionScale*pt[3],regionScale*pt[3])
        zRange = (-0.5,0.5) if pt[5] == 0 else (-regionScale*pt[5],regionScale*pt[5])
        pt = (pt[0]+random.uniform(*xRange),pt[1]+1.0,pt[2]+random.uniform(*zRange))
        bs.gameTimer(random.randrange(2000),bs.WeakCall(self._spawnBombAtPt,pt))
            
    def _spawnBombAtPt(self,pt):
        if self.hasEnded(): return
        bs.Bomb(position=pt,bombType='normal').autoRetain()
        
    def _makeMine(self,i):
        m = self._raceMines[i]
        m['mine'] = bs.Bomb(position=m['point'][:3],bombType='landMine')
        m['mine'].arm()

    def _flashMine(self,i):
        m = self._raceMines[i]
        light = bs.newNode("light",
                           attrs={'position':m['point'][:3],
                                  'color':(1,0.2,0.2),
                                  'radius':0.1,
                                  'heightAttenuated':False})
        bs.animate(light,"intensity",{0:0,100:1.0,200:0},loop=True)
        bs.gameTimer(1000,light.delete)
        
    def _updateRaceMine(self):
        for i in range(3):
            mIndex = random.randrange(len(self._raceMines))
            m = self._raceMines[mIndex]
            if m['mine'] is None or not m['mine'].exists(): break
        if m['mine'] is None or not m['mine'].exists():
            self._flashMine(mIndex)
            bs.gameTimer(950,bs.Call(self._makeMine,mIndex))

    def spawnPlayer(self,player):

        # dont allow spawning if this team is done
        if player.getTeam().gameData['finished']: return

        pt = self._regions[player.gameData['lastRegion']]._pt
        regionScale = 0.8 # dont use the full region so we're less likely to spawn off a cliff
        xRange = (-0.5,0.5) if pt[3] == 0 else (-regionScale*pt[3],regionScale*pt[3])
        zRange = (-0.5,0.5) if pt[5] == 0 else (-regionScale*pt[5],regionScale*pt[5])
        pt = (pt[0]+random.uniform(*xRange),pt[1],pt[2]+random.uniform(*zRange))
        spaz = self.spawnPlayerSpaz(player,position=pt,angle=90 if not self._raceStarted else None)

        # prevent controlling of characters before the start of the race
        if not self._raceStarted: spaz.disconnectControlsFromPlayer()

        m = bs.newNode('math',owner=spaz.node,attrs={'input1':(0,1.4,0),'operation':'add'})
        spaz.node.connectAttr('torsoPosition',m,'input2')
        spaz.distanceTxt = bs.newNode('text',
                                      owner=spaz.node,
                                      attrs={'text':'',
                                             'inWorld':True,
                                             'color':(1,1,0.4),
                                             'scale':0.02,
                                             'hAlign':'center'})
        m.connectAttr('output',spaz.distanceTxt,'position')

    def _checkEndGame(self):

        # if there's no teams left racing, finish
        teamsStillIn = len([t for t in self.teams if not t.gameData['finished']])
        if teamsStillIn == 0:
            self.endGame()
            return

        # count the number of teams that have completed the race
        teamsCompleted = len([t for t in self.teams if t.gameData['finished'] == True and t.gameData['time'] is not None])

        if teamsCompleted > 0:
            # in teams mode its over as soon as any team finishes the race
            if isinstance(self.getSession(),bs.TeamsSession):
                self.endGame()
            else:
                # in ffa we keep the race going while there's still any points to be handed out.
                # find out how many points we have to award and how many teams have finished,
                # and once that matches we're done
                pointsToAward = len(self.getSession()._getFFAPointAwards())
                if teamsCompleted >= pointsToAward-teamsCompleted:
                    self.endGame()
                    return
        
    def endGame(self):

        # stop updating our time text, and set it to show the exact last finish time if we have one..
        # (so users dont get upset if their final time differs from what they see onscreen by a tiny bit)
        if self._timer.hasStarted():
            self._timer.stop(endTime=None if self._lastTeamTime is None else (self._timer.getStartTime()+self._lastTeamTime))
        
        results = bs.TeamGameResults()
        
        for t in self.teams: results.setTeamScore(t,t.gameData['time'])
        # we don't announce a winner in ffa mode since its probably been a while since the first place guy
        # crossed the finish line so it seems odd to be announcing that now..
        self.end(results=results,announceWinningTeam=True if isinstance(self.getSession(),bs.TeamsSession) else False)

    def handleMessage(self,m):
        if isinstance(m,bs.PlayerSpazDeathMessage):
            bs.TeamGameActivity.handleMessage(self,m) # augment default
            try:
                player = m.spaz.getPlayer()
                if player is None:
                    bs.printError('FIXME: getPlayer() should no longer ever be returning None')
                else:
                    if not player.exists(): raise Exception()
                team = player.getTeam()
            except Exception: return
            if not player.gameData['finished']: self.respawnPlayer(player,respawnTime=1000)
        else:
            bs.TeamGameActivity.handleMessage(self,m)

